NodeJS 公式提供のworker_threads
モジュールを使うとマルチスレッドで実行されるコードを書けます。
スレッドの作成
Worker
クラスから作ります。このコンストラクタには第一引数に別スレッドで実行するjs
ファイルへのパス、第二引数(オプショナル)でその別スレッドに渡す値が渡せます。
const {Worker} = require('worker_threads');
new Worker('/path/to/ping-pong.js', {
workerData: {
value: 'ping'
}
});
送られてきたデータはworker_threads
のworkerData
で受け取れます。このオブジェクトはコンストラクタで渡したworkerData
と同じ構造を持ちます。
例えば/path/to/ping-pong.js
では以下のworkerData
箇所のようにその値を使います。
const {isMainThread, workerData, parentPort} = require('worker_threads');
if (isMainThread) {
throw new Error("メインスレッドでは実行できません");
}
const {value} = workerData;
if (value === 'ping') {
parentPort.postMessage({value: 'pong'});
}
/path/to/ping-pong.js
はメインスレッドでの実行を想定してません。isMainThread
でboolean
値で「メインスレッドでの実行か?」が判断できます。
その気になればこの判断を使って、一つの.js
ファイルにメインスレッドの処理と別スレッドの処理の両方を書けます。
あるスレッドからメインスレッドへデータを送り返すにはparentPost.postMessage(data)
を使います。これによりメインスレッドのWorker#on
のmessage
イベントのハンドラーでdata
を同じ構造で受け取れます。
Worker#on
では他にerror
とexit
イベントも扱えます。これらはそれぞれ、あるスレッドの中でエラーが投げられた時や、コードが終了させられた場合に呼ばれます。
ここでは大げさにスレッドを分割してみて、
別スレッドで MarkDown ファイル
README.md
を取得別スレッドで MarkDown から HTML に変換
結果を表示
のようなプログラムを書いて使い方を見てみます。
README.md を取得
const {
Worker,
isMainThread,
parentPort
} = require("worker_threads");
const path = require("path");
const fs = require("fs");
const { promisify } = require("util");
const readFile = promisify(fs.readFile);
if (isMainThread) {
/**
* @returns {Promise} README.md の中身を持つ Promise
*/
const readReadme = () => {
return new Promise((resolve, reject) => {
new Worker(__filename)
.on("message", ({ contents }) => resolve(contents))
.on("error", error => reject(error));
});
};
(async () => {
// ...
// 略
// ...
})();
} else {
(async () => {
const contents = await readFile(
path.resolve(__dirname, "README.md"),
"utf-8"
);
parentPort.postMessage({ contents });
})();
}
1つのファイルで2つのスレッドの処理内容を書いてます。readReadme
によって別スレッドが建てられ、そこでREADME.md
を読み込み、内容を返してます。__filename
のように Worker には実行中のファイルと同じ名前のファイル名を渡します。このスクリプトは二度実行されることになりますが、isMainThread
により処理を切り分けてます。
別スレッドで MarkDown から HTML に変換
今度は別ファイル<project-root>/workers/compile-markdown-to-html.js
で書いてみます。この中身は以下の通りです。ごちゃごちゃしてますが「contents
という MarkDown で書かれたテキストを受け取り、HTML に変換してメインスレッドに返す」ということをやってます。
const {
isMainThread,
parentPort,
workerData
} = require("worker_threads");
if (isMainThread) {
throw new Error("メインスレッドでは実行できません");
}
(async () => {
const unified = require("unified");
const markdown = require("remark-parse");
const remark2rehype = require("remark-rehype");
const minify = require("rehype-preset-minify");
const stringify = require("rehype-stringify");
const { contents } = workerData;
unified()
.use(markdown)
.use(remark2rehype)
.use(minify)
.use(stringify)
.process(contents, (err, file) => {
if (err !== null) {
throw err;
}
parentPort.postMessage({ file });
});
})();
メインスレッド側に以下のコードを追記します。compileMarkDownToHTML
は、compile-markdown-to-html.js
から別スレッドで MarkDown なテキストと共に建て、変換が終わったら結果を返す非同期関数です。
/**
* @returns {Promise} HTML を持つ Promise
*/
const compileMarkDownToHTML = contents => {
return new Promise((resolve, reject) => {
new Worker(
path.resolve(
__dirname,
"workers/compile-markdown-to-html.js"
),
{
workerData: {
contents
}
}
)
.on("message", ({ file }) => resolve(file.contents))
.on("error", error => reject(error))
});
};
結果を表示
では今まで作ったreadReadme
とcompileMarkDownToHTML
を使って変換後結果を表示します。
(async () => {
const readmeContents = await readReadme();
const html = await compileMarkDownToHTML(readmeContents);
console.log(html);
})();
リポジトリ
上記のコードはリポジトリからgit clone
後yarn && node main.js
で実際に走らせれます。